package fag.core.collision;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import fag.core.geometry.Vector2D;
import fag.core.geometry.Winding;

/**
 * Algorithme de clipping. Permet l'obtention du patch de contact.
 * @author lumz.
 */
public class Clipping {
	
	/**
	 * Classe interne de gestion des primitives de contact formes géométriques impliquées dans la collision.
	 * @author lumz.
	 */
	public static class Feature {
		
		private Winding winding = Winding.COUNTERCLOCKWISE;
		
		private List<Vector2D> vertices = new ArrayList<Vector2D>(2);
		
		/**
		 * Constructeur utilisé lorsque la collision implique qu'un seul et unique point de la primitive géométrique.
		 * @param vertex Le sommet d'assistance.
		 */
		public Feature(Vector2D vertex) {
			vertices.add(vertex);
		}
		
		/**
		 * Constructeur utilisé lorsque la collision implique une arête de la primitive géométrique.
		 * @param a Le premier sommet.
		 * @param b Le second sommet.
		 * @param winding Le sens de l'enroulement.
		 */
		public Feature(Vector2D a, Vector2D b, Winding winding) {

			this.winding = winding;
			vertices.add(a);
			vertices.add(b);
		}
		
		/**
		 * Accesseur des sommets de la primitive.
		 */
		public Vector2D get(int index) {
			return vertices.get(index);
		}
		
		/**
		 * Accesseur de l'enroulement.
		 * @return L'enroulement de cette primitive.
		 */
		public Winding getWinding() {
			return winding;
		}
		
		/**
		 * Obtenir le nombre de points dans la primitive.
		 * @return Le nombre de points dans la primitive.
		 */
		public int size() {
			return vertices.size();
		}
		
		/**
		 * Convertir cette primitive en vecteur 2D.
		 * @return Un vecteur de longueur nulle s'il s'agit d'un sommet, le vecteur de l'arête s'il s'agit d'une arête.
		 */
		public Vector2D toVector2D() {
			
			// Il s'agit d'un sommet
			if(size() == 0)
				return new Vector2D();
			// Il s'agit d'une arête
			else
				return get(1).sub(get(0));		
		}
	}
	
	/**
	 * Constructeur par défaut.
	 */
	private Clipping() {
	}
	
	/**
	 * Opération de découpage d'un segment.
	 * @param v1 Le premier sommet de l'arête.
	 * @param v2 Le second sommet de l'arête.
	 * @param normal La normale indiquant l'angle de coupe.
	 * @param offset La distance à l'origine de la coupe le long de la normale.
	 * @return Le nouveau segment après la découpe.
	 */
	public static List<Vector2D> clip(Vector2D v1, Vector2D v2, Vector2D normal, double offset) {
		
		final List<Vector2D> clip = new ArrayList<Vector2D>(2);
		
		double d1 = normal.dot(v1) - offset;
		double d2 = normal.dot(v2) - offset;
		
		// On garde les sommets qui ne se trouvent pas dans le demi-plan découpé
		if(d1 >= 0) clip.add(v1);
		if(d2 >= 0) clip.add(v2);
		
		// Calculer le point de coupe s'il existe
		if(d1 * d2 < 0) {
			
			Vector2D edge = v2.sub(v1);
			
			// Calcul du paramètre u le long de l'arête
			double u = d1 / (d1 - d2);
			
			Vector2D v3 = edge.mul(u);
			v3 = v3.add(v1);
			
			clip.add(v3);
		}
		
		return clip;
	}
	
	/**
	 * Obtenir le résultat de l'algorithme.
	 * @param c1 Première boîte de collision.
	 * @param c2 Deuxième boîte de collision.
	 * @param normal La normale de collision.
	 * @return Le résultat de l'algorithme de clipping.
	 */
	public static List<Vector2D> getResult(Hitbox h1, Hitbox h2, Vector2D normal) {
		
		List<Vector2D> contactManifold = new ArrayList<Vector2D>();
		
		Feature f1 = h1.clippingSupport(normal);
		Feature f2 = h2.clippingSupport(normal.mul(-1));
		
		// Vérifier si la collision n'a lieu qu'en un seul est unique point
		if(f1.size() == 1) {
			contactManifold.add(f1.get(0));
			return contactManifold;
		}
		if(f2.size() == 1) {
			contactManifold.add(f2.get(0));
			return contactManifold;
		}
		
		// Convertir les assistants de découpe en arêtes de référence et d'incidence
		boolean flip = false;
		
		Feature reference = null;
		Feature incident = null;
		
		if(f1.toVector2D().dot(normal) <= f2.toVector2D().dot(normal)) {
			reference = f1;
			incident = f2;
		}
		else {
			reference = f2;
			incident = f1;
			flip = true;
		}
		
		// Découpage
		Vector2D clipNormal = reference.toVector2D().normalize();
		double offset = 0;
		
		offset = clipNormal.dot(reference.get(0));
		contactManifold = clip(incident.get(0), incident.get(1), clipNormal, offset);
		if(contactManifold.size() < 2) return new ArrayList<Vector2D>();
		
		offset = clipNormal.dot(reference.get(1));
		contactManifold = clip(contactManifold.get(0), contactManifold.get(1), clipNormal.mul(-1), -offset);
		if(contactManifold.size() < 2) return new ArrayList<Vector2D>();

		assert(f1.getWinding().equals(f2.getWinding()));
		
		if(f1.getWinding() == Winding.COUNTERCLOCKWISE) clipNormal = clipNormal.normal(Winding.CLOCKWISE);
		else clipNormal = clipNormal.normal(Winding.COUNTERCLOCKWISE);

		if(flip) clipNormal = clipNormal.mul(-1);
		double max = Math.max(clipNormal.dot(reference.get(0)), clipNormal.dot(reference.get(1)));
		
		Iterator<Vector2D> it = contactManifold.iterator();
		while(it.hasNext())
			if(clipNormal.dot(it.next()) < max) it.remove();
		
		return contactManifold;	
	}
}
